﻿using System.Diagnostics;
using System.Threading.Channels;

namespace WaferService
{
    record Operation(int Foup, int Slot, Task Task);

    enum Command
    {
        HealthCheck,
        Unload
    }

    public class WaferLoadingQueue(IEFEMService efemService,
        IPLCService plcService,
        IMotionControlService motionControlService,
        IHVServiceClient hvClient,
        IEChuckServiceClient eChuckServiceClient,
        IEventStream eventStream,
        LoadingPositionShim shim,
        ILogger<WaferLoadingQueue> logger)
    {
        readonly CancellationTokenSource cts = new();
        readonly Channel<Command> processing = Channel.CreateBounded<Command>(1);

        volatile bool completing;
        volatile bool completed;

        int awaiting = 0;

        int arm1 = 1;
        int arm2 = 2;

        readonly Queue<Task<Substrate>> queue = [];

        public double AlignerAngle { get; set; }

        public bool UseEChuck { get; set; }

        public bool UseXYStage { get; set; }

        public async Task Unload(CancellationToken tok)
        {
            var v = Interlocked.Exchange(ref awaiting, 0);
            if (v == 0)
            {
                throw new Exception("Wafer not ready yet.");
            }
            await processing.Writer.WriteAsync(Command.Unload, tok);
        }

        public async Task<bool> HealthCheck(CancellationToken tok)
        {
            try
            {
                await processing.Writer.WriteAsync(Command.HealthCheck, tok);
                return true;
            }
            catch (Exception)
            {
                return false;
            }
        }

        public void RequestCompletion()
        {
            completing = true;
        }

        public void Cancel()
        {
            completed = true;
            processing.Writer.TryComplete();
        }

        public async Task LoadAll(List<(int, int)> slots)
        {
            try
            {
                await eventStream.StartTransfer();
                if (UseXYStage)
                {
                    await hvClient.StageHVOFF();

                    await motionControlService.SwitchToEncoderPosition();
                    logger.LogInformation("MoveToLL: Motion Switch to Encoder Mode done.");
                    await motionControlService.GoToLoadingPos();
                    logger.LogInformation("MoveToLL: Motion Move to LoadingPosition.");
                }

                shim.Start();

                var prev = Task.FromResult(new Substrate());
                for (var i = 0; i < slots.Count; i++)
                {
                    if (completed)
                    {
                        return;
                    }

                    var (foup, n) = slots[i];
                    var sub = await Load(new Substrate
                    {
                        Foup = foup,
                        Slot = n,
                        Location = WaferLocation.Foup
                    });

                    if (completed)
                    {
                        return;
                    }

                    if (i + 1 == slots.Count)
                    {
                        await plcService.SetLastWafer(true);
                    }
                    else if (completing)
                    {
                        await plcService.SetLastWafer(true);
                        prev = ExchangeLast(prev, sub);
                        queue.Enqueue(prev);
                        break;
                    }

                    prev = Process(prev, sub);
                    queue.Enqueue(prev);

                    (arm1, arm2) = (arm2, arm1);
                }

                while (queue.Count > 0)
                {
                    if (completed)
                    {
                        return;
                    }

                    var sub = await queue.Dequeue();

                    if (queue.Count > 0)
                    {
                        // second last wafer was swaped out
                        await MoveToFoup(new Substrate
                        {
                            Foup = sub.Foup,
                            Slot = sub.Slot,
                            Location = WaferLocation.LoadLock
                        });
                    }
                    else
                    {
                        await MoveToFoup(sub);
                    }
                }

                await eventStream.EndTransfer();
            }
            finally
            {
                Cancel();
                await AwaitCompletion();
            }
        }

        async Task AwaitCompletion()
        {
            while (queue.Count > 0)
            {
                try
                {
                    await queue.Dequeue();
                }
                catch (Exception)
                {
                    // ignored
                }
            }
            shim.Stop();
        }

        async Task<Substrate> Load(Substrate sub)
        {
            await using var scope = eventStream.NewScope(sub);

            await efemService.Load(Parameter.LoadPort(sub.Foup, sub.Slot), Parameter.Arm(arm1));
            await scope.MoveTo(arm1 == 1 ? WaferLocation.Arm1 : WaferLocation.Arm2, WaferState.Loading);

            await efemService.UnLoad(Parameter.Aligner(), Parameter.Arm(arm1));
            await scope.MoveTo(WaferLocation.Aligner);

            await efemService.Align(Parameter.Angle(AlignerAngle));

            await efemService.Load(Parameter.Aligner(), Parameter.Arm(arm1));
            await scope.MoveTo(arm1 == 1 ? WaferLocation.Arm1 : WaferLocation.Arm2);

            if (queue.Count < 2)
            {
                var present = await AsyncTimeout.Run(TimeSpan.FromSeconds(60), plcService.OpenLoadLockDoor);
                if (present)
                {
                    throw new Exception("An Unexpected wafer in load lock");
                }

                await efemService.UnLoad(Parameter.LoadLock(), Parameter.Arm(arm1));
                await scope.MoveTo(WaferLocation.LoadLock);
            }
            else
            {
                Substrate subOut;
                try
                {
                    subOut = await queue.Dequeue();
                }
                catch (Exception)
                {
                    scope.Complete();
                    throw;
                }

                var present = await AsyncTimeout.Run(TimeSpan.FromSeconds(60), plcService.OpenLoadLockDoor);
                if (!present)
                {
                    throw new Exception("Expected a wafer in load lock");
                }

                await efemService.Change(Parameter.LoadLock(), Parameter.Arm(arm2), Parameter.Arm(arm1));
                var arm = arm2 == 1 ? WaferLocation.Arm1 : WaferLocation.Arm2;
                // the outgoing state is out of date
                await scope.Swap(WaferLocation.LoadLock, subOut.Foup, subOut.Slot, arm);
                await efemService.UnLoad(Parameter.LoadPort(subOut.Foup, subOut.Slot), Parameter.Arm(arm2));
                await scope.MoveTo(subOut.Foup, subOut.Slot, arm, WaferLocation.Foup, WaferState.Finished);
            }

            scope.Complete();
            return scope.Substrate;
        }

        async Task MoveToFoup(Substrate sub)
        {
            await AsyncTimeout.Run(TimeSpan.FromSeconds(60), plcService.GetWaferStart);

            await using var scope = eventStream.NewScope(sub);
            if (sub.Location != WaferLocation.LoadLock)
            {
                await scope.MoveTo(WaferLocation.LoadLock);
            }

            await efemService.Load(Parameter.LoadLock(), Parameter.Arm(arm2));
            await scope.MoveTo(arm2 == 1 ? WaferLocation.Arm1 : WaferLocation.Arm2);

            await efemService.UnLoad(Parameter.LoadPort(sub.Foup, sub.Slot), Parameter.Arm(arm2));
            await scope.MoveTo(WaferLocation.Foup, WaferState.Finished);
            scope.Complete();
        }

        async Task<Substrate> Process(Task<Substrate> prev, Substrate sub)
        {
            // ensure lift pin is up at this point
            var subOut = await prev;

            await using var scope = eventStream.NewScope(sub);
            var timeout = new AsyncTimeout();
            await timeout.Execute(TimeSpan.FromSeconds(60), plcService.PutWaferEnd);
            logger.LogInformation("Lifter pin down");

            if (subOut.Foup > 1)
            {
                await scope.Swap(WaferLocation.Stage, subOut.Foup, subOut.Slot, WaferLocation.LoadLock);
            }
            else
            {
                await scope.MoveTo(WaferLocation.Stage);
            }

            if (completed)
            {
                throw new OperationCanceledException();
            }

            if (completing)
            {
                await scope.SetState(WaferState.Unloading);
            }
            else
            {
                if (UseEChuck)
                {
                    await eChuckServiceClient.PutWaferValue();
                    await eChuckServiceClient.AdsorptionAction();
                    await eChuckServiceClient.AdsorptionCheck();
                    logger.LogInformation("EChuck Chuck wafer done.");
                    await eChuckServiceClient.PunctureAction();
                    logger.LogInformation("EChuck Puncture wafer done.");
                }

                if (UseXYStage)
                {
                    await motionControlService.SetAbsolutePosition(155, 205);
                    await motionControlService.SwitchToLaserPosition();
                    logger.LogInformation("Motion Switch to Laser Mode done.");
                }

                await scope.SetState(WaferState.Processing);
                Interlocked.Exchange(ref awaiting, 1);
                await AwaitProcessingCompletion();
                await scope.SetState(WaferState.Unloading);

                if (UseXYStage)
                {
                    await hvClient.StageHVOFF();

                    await motionControlService.SwitchToEncoderPosition();
                    logger.LogInformation("Motion Switch to Encoder Mode done.");
                    await motionControlService.GoToLoadingPos();
                    logger.LogInformation("Motion Move to LoadingPosition.");
                }

                if (UseEChuck)
                {
                    await eChuckServiceClient.ReleaseAction();
                    logger.LogInformation("DeChuck wafer done.");
                }
            }

            await plcService.SetWaferDetectionFinished();
            scope.Complete();
            return scope.Substrate;
        }

        async Task AwaitProcessingCompletion()
        {
            for (; ; )
            {
                cts.CancelAfter(TimeSpan.FromSeconds(15));
                try
                {
                    var cmd = await processing.Reader.ReadAsync(cts.Token);
                    Debug.Assert(cts.TryReset());
                    switch (cmd)
                    {
                        case Command.HealthCheck:
                            break;
                        case Command.Unload:
                            return;
                    }
                }
                catch (OperationCanceledException)
                {
                    logger.LogWarning("About to shutdown due to inactivity.");
                    throw;
                }
            }
        }

        async Task<Substrate> ExchangeLast(Task<Substrate> prev, Substrate sub)
        {
            // ensure lift pin is up at this point 
            var subOut = await prev;

            if (completed)
            {
                throw new OperationCanceledException();
            }

            await using var scope = eventStream.NewScope(sub);
            await AsyncTimeout.Run(TimeSpan.FromSeconds(60), plcService.PutWaferEnd);
            if (subOut.Foup > 1)
            {
                await scope.Swap(WaferLocation.Stage, subOut.Foup, subOut.Slot, WaferLocation.LoadLock);
            }
            else
            {
                await scope.MoveTo(WaferLocation.Stage);
            }

            await plcService.SetWaferDetectionFinished();
            await scope.SetState(WaferState.Unloading);
            scope.Complete();
            return scope.Substrate;
        }
    }
}
